העלאת קבצים ותמונות ב-php, שמירת קבצים ותמונות במסד והעלאה ב-ajax
כל אלה — בכתבה ארוכה אחת.
העלאת קבצים ותמונות ב-php יכול להסתכם בכמה שורות קוד בודדות בזכות האפשרויות של השפה. התהליך מורכב משלושה חלקים, בניית טופס שהדפדפן ידע לשלוח, העלאה עצמה, אותה עושה php ועבודה עם הקובץ המועלה שנשארת בידינו.
במדריך זה נדבר ונדגים על "העלאת קבצים ב-ajax" ונבין למה לא קיים דבר כזה בטבע ומה אפשר לעשות במקום. נדבר גם על אבטחת הקבצים הנכנסים, איך לשמור אותם במסד, או ליתר דיוק למה לא צריך לעשות את זה ונראה איזה הגדרות של php כדאי לנו לשנות.
טופס העלאת קבצים
לרוב תהליך העלאה php והשרת דואגים בעצמם. הם מצפים מאיתנו רק שנספק להם את הנתונים המתאימים (הפלט) ונקבל מהם הכל מוכן. תפקידו של הדפדפן הוא להעביר את הקבצים שנמצאים על המחשב שלכם לשרת, כדי שמשם יטופל על ידי מי שצריך. כמו כל הנתונים שנשלחים לשרת גם קבצים נשלחים דרך טפסים <form> רגילים, עם שינוי אחד קטן.
הטופס חייב להכיל 3 מרכיבים הבאים:
1. הטופס חייב להישלח בעזרת POST ולא בעזרת GET
2. הטופס חייב להכיל אטריבות enctype עם הערך multipart/form-data
3. הטופס יכיל שדה מסוג <input type=file>
<form method="post" enctype="multipart/form-data">
<input type="text" name="text" />
<input type="file" name="upfile" />
<input type="submit" value="Upload me" />
</form>
<input type="text" name="text" />
<input type="file" name="upfile" />
<input type="submit" value="Upload me" />
</form>
enctype = multipart/formdata
האטריבות enctype גורם לדפדפן לשלוח לשרת header שאומר מה סוג הנתונים שמגיעים אליו. multipart/form-data זהו סוג נתונים המורכב מכמה חלקים שונים ומגיע מטופס. השרת יודע לזהות כותר זה ולהתנהג אל המידע הזה בהתאם. בלעדיו יהיו מקרים שהשרת יקבל רק את הקובץ ויתעלם משאר נתוני הטופס או להפך.
משתנה $_FILES
אחרי שלחצנו "שלח" בטופס שלנו, הדפדפן שולח את הקובץ ואת הטופס לשרת, השרת שם את הקובץ בתיקיה זמנית ומריץ את הסקריפט שבו נוצר מערך עם הנתונים על הקובץ. שימו לב, השרת מקבל את הקובץ, שם אותו בתיקיה זמנית ורק אז מריץ את הסקריפט עם מערך מאותחל בו הנתונים על הקובץ.
כמו כל שאר הנתונים שמגיעים לסקריפטים מטפסים, גם מידע על קבצים שהועלו מגיע לסקריפט בתוך מערך מיוחד הנקרא $_FILES;. בואו נראה מה מכיל המערך הזה בעזרת ניסוי פשוט. נעלה לשרת קובץ, ונדפיס בסקריפט את תוכן המערך, באמצעות הקוד הבא:
<form method="post" enctype="multipart/form-data">
<input type="text" name="text" />
<input type="file" name="upfile" />
<input type="submit" value="Upload me" />
</form>
<?php
if (isset($_POST['text']))
{
print_r($_FILES);
}
<input type="text" name="text" />
<input type="file" name="upfile" />
<input type="submit" value="Upload me" />
</form>
<?php
if (isset($_POST['text']))
{
print_r($_FILES);
}
הפלט שלי נראה ככה:
Array
(
[upfile] => Array
(
[name] => backup.sql
[type] => text/x-sql
[tmp_name] => C:\Temp\php123.tmp
[error] => 0
[size] => 347168
)
)
(
[upfile] => Array
(
[name] => backup.sql
[type] => text/x-sql
[tmp_name] => C:\Temp\php123.tmp
[error] => 0
[size] => 347168
)
)
בדוגמה הזו אנחנו רואים מערך שבתוכו נמצא עוד מערך עם המפתח upfile, שהוא השם של השדה שרשמנו בטופס. אם נשנה את שם השדה בטופס ישתנה גם המפתח של המערך בהתאם. והנה לכם עבודה עצמית: שנו את הטופס כך שיופיעו בו שני שדות מסוג <input type=file>, העלו שני קבצים במקביל לשרת וראו מה מכיל המערך עכשיו.
התוצאה של פעולה זו היא מערך $_FILES שמכיל שני תת מערכים. כל אחד מהמערכים הפנימיים מכיל חמישה שדות. פירוט קצר לגבי כל אחד מהם:
1. שם — הוא השם שנשא הקובץ במחשב של המשתמש. יכול להיות שהמשתמש שינה את שם הקובץ לשם אחר שניה לפני שאעלה את הקובץ לאתר מ-hack.php למשהו פחות חשוד. בכל אופן, השם לא משחק שום תפקיד מעניין בתהליך ולא מהווה שום מידע אמין.
2. סוג — סוג הקובץ שהועלה. הסוג של הקובץ מתגלה על פי הסיומת של הקובץ ולכן גם הוא אינו מהווה מידע אמין על סוג הקובץ.
3. שם זמני — הוא השם שתחתיו השרת שמר את הקובץ בתיקיה הזמנית שלו, כדי שניקח אותו אחר-כך משם בעצמנו.
4. error — שגיאה בהעלאה. אם השרת יודע שצריך לקבל קובץ, אבל לא מצליח לשמור אותו, תופיע כאן שגיאה כלשהי. קיימות שמונה שגיאות מספריות אפשריות שרשומות בעמוד הזה, כאשר המספר 0 מסמן כי לא היו שגיאות, לכן ניתן לבדוק האם שדה זה גדול מאפס ולוודא שהקובץ עלה.
5. גודל הקובץ בבייטים. יש לחלק ב-1024 כדי לקבל kb ובעוד 1024 כדי להגיע לגודל הקובץ ב-mb. נוכל להשתמש בשדה זה כדי להגביל את גודל הקובץ המקסימלי.
בדיקות
לאחר שהקובץ עלה והמערך נוצר, נוכל להשתמש במערך זה כדי לוודא שהעלאה התרחשה והכל תקין. הקוד הבא מדגים בדיקת סוג קובץ וגודלו:
<form method="post" enctype="multipart/form-data">
<input type="text" name="text" />
<input type="file" name="upfile" />
<input type="submit" value="Upload me" />
</form>
<?php
if (isset($_POST['text'], $_FILES['upfile']))
{
$allowed = array('image/png', 'image/jpeg', 'image/gif');
$maxsize = 1 * 1024 * 1024; // 1mb
echo 'File ', $_FILES['upfile']['name'], ' was uploaded <br/>';
if( $_FILES['upfile']['error'] > 0)
die("Error #".$_FILES['upfile']['error']." occured");
echo 'It\'s type is: ', $_FILES['upfile']['type'], '<br/>';
if( !in_array($_FILES['upfile']['type'], $allowed) )
die('This type is not allowed');
$size_in_mb = round( $_FILES['upfile']['size']/1024/1024 ,3);
echo 'It\'s size is: ', $size_in_mb , 'mb <br/>';
if($_FILES['upfile']['size'] > $maxsize)
die("The file is bigger then allowed");
echo 'The file passed all validations. <br/>
It has a proper size, it is an image and it had no errors. <br/>';
echo 'Copying the file as "uploaded.png"';
move_uploaded_file($_FILES['upfile']['tmp_name'], './uploaded.png' );
}
<input type="text" name="text" />
<input type="file" name="upfile" />
<input type="submit" value="Upload me" />
</form>
<?php
if (isset($_POST['text'], $_FILES['upfile']))
{
$allowed = array('image/png', 'image/jpeg', 'image/gif');
$maxsize = 1 * 1024 * 1024; // 1mb
echo 'File ', $_FILES['upfile']['name'], ' was uploaded <br/>';
if( $_FILES['upfile']['error'] > 0)
die("Error #".$_FILES['upfile']['error']." occured");
echo 'It\'s type is: ', $_FILES['upfile']['type'], '<br/>';
if( !in_array($_FILES['upfile']['type'], $allowed) )
die('This type is not allowed');
$size_in_mb = round( $_FILES['upfile']['size']/1024/1024 ,3);
echo 'It\'s size is: ', $size_in_mb , 'mb <br/>';
if($_FILES['upfile']['size'] > $maxsize)
die("The file is bigger then allowed");
echo 'The file passed all validations. <br/>
It has a proper size, it is an image and it had no errors. <br/>';
echo 'Copying the file as "uploaded.png"';
move_uploaded_file($_FILES['upfile']['tmp_name'], './uploaded.png' );
}
יצרנו מערך של סוגי קבצים שהתקבלו עלינו ואחר כך בדקנו האם סוג הקובץ שהועלה נמצא במערך הזה. אם נמצא — הקובץ הוא מסוג תקין ונאפשר לו לעבור עלאה.
move_uploaded_file
הפונקציה הזו מאוד פשוטה. היא מקבלת את נתיב הקובץ בתיקיה הזמנית (ששמור במערך $_FILES) ומעתיקה את הקובץ תוך שינוי שם למיקום אחר, כפי שנגדיר לו. הנקודה-סלאש בהתחלה מסמנות לסקריפט להעתיק את הקובץ לתיקיה הנוכחית תחת השם uploaded.png.
נכון שבפועל הקובץ הוא לאו דווקא קובץ png, אבל חשוב להבין שאין שום קשר בין השם ו/או סיומת השם של הקובץ, לבין מה, שהוא מכיל בפנים. לצורך העניין תמיד תוכלו לשנות את את השם של הסקריפט hack.php לשם goodimg.gif
אבל התוכן של הקובץ יישאר כפי שהיה. כך גם פה, השם משתנה, אך התוכן נשאר זהה.
אבל אם אנחנו יכולים לשנות בכזו קלות את "סוג" הקובץ שהשרת חושב שהוא, האם זה לא מסוכן? כן, מסוכן. השאלה היא מה אתם עושים עם הקובץ הזה. כדי שהקובץ hack.php יזיק לכם, לא מספיק שהוא יימצא בשרת, אלה צריך גם להפעיל אותו. אם תעתיקו אותו לתיקיה, שיש אליה גישה מהדפדפן, תשמרו על השם המקורי של הקובץ, יכול מאוד להיות שמישהו יפעיל אותו בכוונה או בטעות.
קבצים שעתידים להגיע למשתמש דרך הדפדפן כמו תמונות רצוי לבדוק לעומק. הפונקציה getimagesize יכולה לשמש לבדיקה האם הקובץ הוא תמונה או לא. קבצים שמגיעים לשרת לשמירה בלבד ולא מופעלים על ידי השרת או הדפדפן אינם מזיקים, ובמידה ואתם פשוט יוצרים איחסון קבצים — אין צורך לבדוק כל קובץ לסוגו.
שמירת קבצים במסד
חד וחלק - קבצים לא שומרים במסד. מסדי נתונים תומכים בשמירת כמויות גדולות של נתונים בינאריים, אבל המקום של קבצים הוא על הדיסק הקשיח ולא כחלק מתוך קבצי המסד.
את הקבצים עצמם יש לשמור בתיקיה כלשהי כמו uploaddir על השרת ובמידת הצורך להגביל את הגישה אליה מבחוץ באמצעות יצירת קובץ .htaccess כזה בתוך התיקיה:
order deny, allow
deny from all
deny from all
אין צורך לשמור על השם המקורי של הקבצים בתיקיה, אך רצוי לשמור על השם המקורי של הקובץ במסד. להפך, בתיקיה רצוי לתת לכל קובץ שם ייחודי שלא יקרה מצב שלשני קבצים יהיה את אותו השם. בדרך כלל שם הקובץ הוא זמן שבו הועלה הקובץ. לא יכולים להיות שני קבצים שהועלו לשרת באותו רגע ממש, לכן שמות הקבצים יישאר ייחודיים.
אחרי שהנתונים על הקובץ נשרו במסד והקובץ עצמו בתיקיה שאין אליה גישה מבחוץ, נשאלת השאלה איך לאפשר להוריד את הקובץ הזה. במסד נשמור גם את שם הקובץ כפי שהוא בדיסק וגם את שם הקובץ המקורי (וגם מזהה id ייחודי של השורה במסד) וכאשר מישהו יבקש להוריד את הקובץ עם ה-id המסוים — נוכל לשלוף את שם הקובץ המקורי וגם את שם הקובץ בתיקיה, לגשת אליו ולהציע לדפדפן להוריד אותו עם השם המקורי.
שימו לב, בקוד שנמצא בקישור ניתן להגדיר שמות שונים איזה קובץ לשלוח ותחת איזה שם להציע אותו. נשלח את הקובץ השמור בתיקיה תחת השם המקורי ששמור במסד.
אם אתם לא מתכוונים לאפשר הורדה של קבצים ולא בונים שירות העלאת קבצים, ובכלל בונים את המערכת לשימושכם האישי, אין צורך לשמור נתונים על קבצים במסד בכלל. פשוט העלו את הקובץ, שימרו אותו בתיקיה שלכם תחת השם המקורי של הקובץ והשתמשו בו כרצונכם.
שליחת קבצים ב-ajax
אין דבר כזה. AJAX - Asynchronius javascript and xml לא כוללת שום דבר בהגדרה שלה על קבצים ובכלל לא כוללת אפשרות העברת קבצים כלשהי.
הפתרון המושלם במקרים כאלה הוא דווקא ה-iframe שמשמש כעמוד בפני עצמו בתוך עמוד בדפדפן. השליחה מתבצעת כביכול דרך iframe שרק הוא מתרענן בזמן ששאר העמוד נשאר ללא שינוי. הקוד של השרת זהה לחלוטין לקוד הקודם, אילו הקוד של הדפדפן משתנה מאט, כדי לגרום לדפדפן לבצע את השליחה ולרענן רק את הפריים ולא את כל העמוד.
index.html
<form method="post" enctype="multipart/form-data" target="uploadframe" action="upload.php">
<input type="text" name="text" />
<input type="file" name="upfile" />
<input type="submit" value="Upload me" />
</form>
<script type="text/javascript">
function getContentFromIframe(iFrameName)
{
var myIFrame = document.getElementById(iFrameName);
var content = myIFrame.contentWindow.document.body.innerHTML;
content = content.split(";;;");
content = content[0];
alert ( content );
}
</script>
<iframe id="uploadframe" name="uploadframe" src="about:blank"
style="visibility:hidden; height:0px; width:0px; border:none;" ></iframe>
<input type="text" name="text" />
<input type="file" name="upfile" />
<input type="submit" value="Upload me" />
</form>
<script type="text/javascript">
function getContentFromIframe(iFrameName)
{
var myIFrame = document.getElementById(iFrameName);
var content = myIFrame.contentWindow.document.body.innerHTML;
content = content.split(";;;");
content = content[0];
alert ( content );
}
</script>
<iframe id="uploadframe" name="uploadframe" src="about:blank"
style="visibility:hidden; height:0px; width:0px; border:none;" ></iframe>
שימו לב לאטריביות target="uploadframe" שנוסף לטופס.
הוא מסמל לדפדפן שיש לשלוח את הטופס דרך הדף (הפריים) עם השם הזה.
הפונקציה ה-javascriptית שהתווספה היא פונקציה שקוראת תוכן של iframe.
כיוון שהדבר היחידי שמתרענן אצלנו זה ה-iframe וכל הפלט של הסקריפט גם נמצא בו - יהיה עלינו לקרוא את הפלט משם. דרישה קטנה אחת היא להשאיר את תג ה-iframe אחרי תג הסקריפט.
upload.php
<?php
if (isset($_POST['text'], $_FILES['upfile']))
{
$allowed = array('image/png', 'image/jpeg', 'image/gif');
$maxsize = 1 * 1024 * 1024; // 1mb
$error = array();
echo 'File ', $_FILES['upfile']['name'], " was uploaded \r\n";
if( $_FILES['upfile']['error'] > 0)
$error[] = "Error #".$_FILES['upfile']['error']." occured";
echo 'It\'s type is: ', $_FILES['upfile']['type'], "\r\n";
if( !in_array($_FILES['upfile']['type'], $allowed) || !getimagesize($_FILES['upfile']['tmp_name']) )
$error[] = 'This type is not allowed';
$size_in_mb = round( $_FILES['upfile']['size']/1024/1024 ,3);
echo 'It\'s size is: ', $size_in_mb , "mb \r\n";
if($_FILES['upfile']['size'] > $maxsize)
$error[] = "The file is bigger then allowed";
if( count($error) == 0)
{
echo "The file passed all validations. \r\n",
"It has a proper size, it is an image and it had no errors. \r\n";
echo 'Copying the file as "uploaded.png"';
move_uploaded_file($_FILES['upfile']['tmp_name'], './uploaded.png' );
}
else
{
echo 'It has failed sove validations:',"\r\n";
echo implode("\r\n", $error);
}
}
?>;;;
<script type='text/javascript'> window.parent.getContentFromIframe('uploadframe'); </script>
if (isset($_POST['text'], $_FILES['upfile']))
{
$allowed = array('image/png', 'image/jpeg', 'image/gif');
$maxsize = 1 * 1024 * 1024; // 1mb
$error = array();
echo 'File ', $_FILES['upfile']['name'], " was uploaded \r\n";
if( $_FILES['upfile']['error'] > 0)
$error[] = "Error #".$_FILES['upfile']['error']." occured";
echo 'It\'s type is: ', $_FILES['upfile']['type'], "\r\n";
if( !in_array($_FILES['upfile']['type'], $allowed) || !getimagesize($_FILES['upfile']['tmp_name']) )
$error[] = 'This type is not allowed';
$size_in_mb = round( $_FILES['upfile']['size']/1024/1024 ,3);
echo 'It\'s size is: ', $size_in_mb , "mb \r\n";
if($_FILES['upfile']['size'] > $maxsize)
$error[] = "The file is bigger then allowed";
if( count($error) == 0)
{
echo "The file passed all validations. \r\n",
"It has a proper size, it is an image and it had no errors. \r\n";
echo 'Copying the file as "uploaded.png"';
move_uploaded_file($_FILES['upfile']['tmp_name'], './uploaded.png' );
}
else
{
echo 'It has failed sove validations:',"\r\n";
echo implode("\r\n", $error);
}
}
?>;;;
<script type='text/javascript'> window.parent.getContentFromIframe('uploadframe'); </script>
שימו לב לשורה האחרונה. היא גורמת לעמוד הנטען להריץ את הפונקציה getContentFromIframe שמוגדרת בעמוד האב המכיל את ה-iframe. כיוון שהפונקציה קוראת את כל תוכן ה-iframe היא תיקרא גם את תוכן תגי הסקריפט שמודפסים בשורה האחרונה.
כדי למנוע את זה — נוספו 3 סימני נקודה-פסיק שמסמנים לפונקציה שכאן נגמר התוכן ומתחילה הפקודה שלנו שאין צורך להתייחס אליה. כמו כן החלפנו את כל ה-die בקוד למערך שרושם שקרתה שגיאה כלשהי. אילו היינו משתמשים ב-die השורה האחרונה של ה-javascript אף פעם לא הייתה קוראת ולא היינו מתעדכנים בנוגע לתוצאות העלאה.
זהו זה, הסקריפט מוכן, אפשר להריץ.
נסו להעלות תמונה ולא תמונה, קבצים בגדלים שונים ותתבוננו בתוצאות.
עדכון: html5 תומכת בהעלאת קבצים ב AJAX
אחד החידושים שהתווספו ב-html5 היא אפשרות העלאת קבצים ב-ajax אמיתי ללא שימוש ב iframe. מידע נוסף באנגלית:
http://blog.new-bamboo.co.uk/2010/7/30/html5-powered-ajax-file-uploads
http://www.sitepoint.com/html5-ajax-file-upload/
תגובות לכתבה:
מסובך רצח .. אני אישית לא הצלחתי .. תודה על המדריך בכל מקרה ..:)
תודה.
יש לי בעיה קטנה.
תיראה עשיתי את שתי הקבצים -index ו- uploded או משהוא כזה לא משנה עשיתי אותם והכל אני בוחר תמונה שהיא בסיומת שאפשרתה לסוגי התמונות וזאת תמונה שהיא מיתחת ל- 1mg וזה לא מציג לי את התמונה זה לא עושה כלום.
עם תוכל לעזור לי תודה.
תודה אלכס,בהחלט יעזור לי ;)
קבל LIKE!!
אבל שים לב שיש לסגור את התג iframe בקובץ index.html.
כי אחרת הבלוקים שיהיו אותו תג, יקבלו ערכי visibility:hidden.
כעת אני עובר על הקוד, ועושה לו כל מיני שיפורים משלי.
אבל באמת שכל הכבוד לך ותודה, עזרת לי מאוד.
אחרי* אותו תג
תודה. סגרתי את התג.
איש יקר
הרבה הרבה תודה זה עובד מצוין
כול הכבוד
אלי אשכנזי
שלום אלכסנדר
אפשר לצור אתך בפרטי ?
[email protected]
תודה
אלי
אפשר. יש ארבעה דרכים ליצירת קשר איתי בתחתית העמוד.
תודה!
יש דרך לבדוק מה השם של הקבצים בתיקייה?
כי אני רוצה לעשות שהשמות שלהן יהי pic1 , pic2 pic3 וכך הלאה.
אז אני רוצה לבדוק מה האחרון (לדוגמה התמונה האחרונה היא pic79 ואני רוצה לקרוא לחדשה pic80.
הבנת?
לבדוק מה השם של הקבצים בתיקייה אפשר באמצעות glob().
עדיף לא לעשות את זה, כי זה פעולה כבדה לסרוק את רשימת הקבצים ולברר לפיה מה הבא.
תשמור במסד אידי של הבא או משהו כזה.
עשיתי את זה:
<form method="post" enctype="multipart/form-data">
<input type="text" name="text" />
<input type="file" name="upfile" />
<input type="submit" value="Upload me" />
</form>
<?php
if (isset($_POST['text'], $_FILES['upfile']))
{
$allowed = array('image/png', 'image/jpeg', 'image/gif');
$maxsize = 1 * 1024 * 1024; // 1mb
echo 'File ', $_FILES['upfile']['name'], ' was uploaded <br/>';
if( $_FILES['upfile']['error'] > 0)
die("Error #".$_FILES['upfile']['error']." occured");
echo 'It\'s type is: ', $_FILES['upfile']['type'], '<br/>';
if( !in_array($_FILES['upfile']['type'], $allowed) )
die('This type is not allowed');
$size_in_mb = round( $_FILES['upfile']['size']/1024/1024 ,3);
echo 'It\'s size is: ', $size_in_mb , 'mb <br/>';
if($_FILES['upfile']['size'] > $maxsize)
die("The file is bigger then allowed");
echo 'The file passed all validations. <br/>
It has a proper size, it is an image and it had no errors. <br/>';
echo 'Copying the file as "uploaded.png"';
move_uploaded_file($_FILES['upfile']['tmp_name'], './uploaded.png' );
}
בשורה $maxsize = 1 * 1024 * 1024; // 1mb
שיניתי ל- $maxsize = 15 * 1024 * 1024; // 15mb
כשאני מנסה להעלות קובץ בגודל 4 מגה אז יש שגיאה Error #1 occured
למה?
יש הגבלה ב php.ini על גודל הקבצים הניתנים להעלאה. לשנות ערכים אלה אפשר באמצעות הוספת שני השורות הבאות ל htaccess
php_value upload_max_filesize 10M
php_value post_max_size 10M
תוווודה ^_^
עכשיו נוצרת לי תמונה.
איך אני משתמש בתמונה בקובץ אחר?
כל הכבוד!
sdsd
כל הכבוד אלכס!
מדריך מעולה!